Перед началом работы необходимо импортировать модуль. В разных частях урока для разных задач нам понадобятся как основной модуль, так и один из его подмодулей, поэтому полный набор инструкций импорта у нас:
import plotly
import plotly.graph_objs as go
import plotly.express as px
from plotly.subplots import make_subplots
Так же нам понадобятся библиотеки Pandas и Numpy для работы с сырыми данными
import plotly
import plotly.graph_objs as go
import plotly.express as px
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd
Начнём с простой задачи построения графика по точкам.
Используем функцию $f(x) = x^2$
Сперва поступим совсем просто и "в лоб":
Создадим график с помощью функции scatter из подмодуля plotly.express (внутрь передадим 2 списка точек: координаты X и Y)
Тут же "покажем" его с помозью метода show()
Обратите внимание - график интерактивный, если навести на него курсор, то можно его приближать и удалять, выделять участки, по наведению курсора на точку получать подробную информацию, возвращать картинку в исходное положение, а при необходимости "скриншотить" и сохранять как файл.
Всё это делается с помощью JS в вашем браузере. А значит, при желании вы можете этим управлять уже после построения фигуры (но мы этого делать пожалуй не будем, т.к. JS != Python)
x = np.arange(0, 5, 0.1)
def f(x):
return x**2
px.scatter(x=x, y=f(x)).show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x)))
fig.show()
С увеличением длины подписи графика, легенда начала наезжать на график. Мне это не нравится, поэтому перенесём легенду вниз.
Для этого применим к фигуре метод update_layout, у которого нас интересует атрибут legend_orientation
fig.update_layout(legend_orientation="h")
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h")
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
title="Plot Title",
xaxis_title="x Axis Title",
yaxis_title="y Axis Title",
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers', name='$$g(x)=x$$'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
def h(x):
return np.sin(x)
def k(x):
return np.cos(x)
def m(x):
return np.tan(x)
fig = go.Figure()
fig.update_yaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='LightPink')
fig.update_xaxes(range=[-0.5, 1.5], zeroline=True, zerolinewidth=2, zerolinecolor='#008000')
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=m(x), name='m(x)=tg(x)'))
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>'))
fig.add_trace(go.Scatter(x=x, y=x, mode='markers',name='g(x)=x',
marker=dict(color='LightSkyBlue', size=20, line=dict(color='MediumPurple', width=3))))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
hovermode="x",
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno')
))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Можно ли добавить больше информации? Конечно можно, но тут возникают хитрости.
Для ещё одного измерения можно использовать размер маркеров.
Важно. Размер - задаётся в пикселях, т.е. величина не отрицательная (в отличие от цвета), поэтому мы будем использовать модуль одной из функций.
так же, величины меньше 2 пикселей обычно плохо видны на экране, поэтому для размера мы добавим множитель.
Размеры задаётся атрибутом size того же словаря внутри marker. Этот атрибут принимает 1 значение (число), либо список (чисел).
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno',
size=50*abs(h(x)))
))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=h(x), name='h(x)=sin(x)'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=abs(h(x)), name='h_mod(x)=|sin(x)|'))
fig.add_trace(go.Scatter(visible='legendonly', x=x, y=k(x), name='k(x)=cos(x)'))
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>'))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
Другой способ задать начальное состояние, слой (с кнопками) и фреймы - сразу передать всё в объект go.Figure:
data - атрибут для графика с начальным состоянием
layout - описание "декораций" включая кнопки
frames - фреймы (кадры) анимации
Мне такой способ кажется менее читаемым за исключением тех случаев, когда вы заранее подготавливаете в отдельных переменных каждый из аргументов до передачи в создаваемую фигуру
fig = go.Figure()
fig.add_trace(go.Scatter(x=[x[0]], y=[f(x)[0]], mode='lines+markers', name='f(x)=x<sup>2</sup>',
marker=dict(color=h(x[0]), colorbar=dict(title="h(x)=sin(x)"), colorscale='Inferno', size=50*abs(h(x[0])))
))
frames=[]
for i in range(1, len(x)):
frames.append(go.Frame(data=[go.Scatter(x=x[:i+1], y=f(x[:i+1]), marker=dict(color=h(x[:i+1]), size=50*abs(h(x[:i+1]))))]))
fig.frames = frames
fig.update_layout(legend_orientation="h",
legend=dict(x=.5, xanchor="center"),
updatemenus=[dict(type="buttons", buttons=[dict(label="Play", method="animate", args=[None])])],
margin=dict(l=0, r=0, t=0, b=0))
fig.update_traces(hoverinfo="all", hovertemplate="Аргумент: %{x}<br>Функция: %{y}")
fig.show()
num_steps = len(x)
fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)'),
go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)')])
frames=[]
for i in range(0, len(x)):
frames.append(go.Frame(name=str(i),
data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)'),
go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)')]))
steps = []
for i in range(num_steps):
step = dict(
label = str(i),
method = "animate",
args = [[str(i)]]
)
steps.append(step)
sliders = [dict(
steps = steps,
)]
fig.update_layout(updatemenus=[dict(direction="left",
x=0.5,
xanchor="center",
y=0,
showactive=False,
type="buttons",
buttons=[dict(label="▶", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
fig.layout.sliders = sliders
fig.frames = frames
fig.show()
Осталось немного облагородить панель слайдера.
Добавим подписи к графику и осям, увеличим и оформим подпись текущего значения слайдера (в других обстоятельствах он стал бы временной шкалой), сместим кнопки анимации влевой, а слайдер чуть сожмём, чтобы освободить им место.
Аргумент currentvalue - задаёт форматирование подписи к текущему шагу, включая префикс, положение на слайде, шрифт
Аргументы x, y, xanchor, yanchor, pad - задают положение и отступы для слайдера и их синтаксис аналогичен таковому у кнопок
num_steps = len(x)
fig = go.Figure(data=[go.Scatter(x=[x[0]], y=[h(x)[0]], mode='lines+markers', name='h(x)=sin(x)',
marker=dict(color=[f(x[0])], colorbar=dict(yanchor='top', y=0.8, title="f(x)=x<sup>2</sup>"), colorscale='Inferno', size=[50*abs(h(x[0]))])),
go.Scatter(x=[x[0]], y=[k(x)[0]], mode='lines+markers', name='k(x)=cos(x)',
marker=dict(color=[f(x[0])], colorscale='Inferno', size=[50*abs(k(x[0]))]))])
frames=[]
for i in range(0, len(x)):
frames.append(go.Frame(name=str(i),
data=[go.Scatter(x=x[:i+1], y=h(x[:i+1]), mode='lines+markers', name='h(x)=sin(x)',
marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(h(x[:i+1])))),
go.Scatter(x=x[:i+1], y=k(x[:i+1]), mode='lines+markers', name='k(x)=cos(x)',
marker=dict(color=f(x[:i+1]), colorscale='Inferno', size=50*abs(k(x[:i+1]))))]))
steps = []
for i in range(num_steps):
step = dict(
label = str(i),
method = "animate",
args = [[str(i)]]
)
steps.append(step)
sliders = [dict(
currentvalue = {"prefix": "Шаг №", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
steps = steps,
)]
fig.update_layout(title="Строим синус и косинус по шагам",
xaxis_title="Ось X",
yaxis_title="Ось Y",
updatemenus=[dict(direction="left",
pad = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="▶", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
fig.layout.sliders = sliders
fig.frames = frames
fig.show()
Возникает вопрос Зачем же мы делали вариант с невидимыми графиками, если они не пригодились?
На самом деле они нужны, в том числе для анимации. Дело в том, что если вы хотите на разных слайдах анимации показывать разное количество графиков, то вам надо в самом начале на этапе создания фигуры добавить столько графиков, сколько их может отображаться максимально. Они все Должны быть невидимыми.
Я специально задам 2 переменные:
graphs_invisible - содержит как невидимый корректный график, так и пустой объект графика вообще без указания видимости
graphs_visible - содержит корректные видимые графики, которые надо показывать по очереди
В первоначальном состоянии мы отобразим невидимые графики или пустышки, а в каждом фрейме будем комбинировать видимые и невидимые, чтобы их количество было постоянным.
graphs_invisible = [go.Scatter(visible = False, x=x, y=f(x), name='f(x)=x<sup>2</sup>'),
go.Scatter(visible = False, x=x, y=x, name='g(x)=x'),
go.Scatter(visible = False, x=x, y=h(x), name='h(x)=sin(x)'),
go.Scatter(visible = False, x=x, y=k(x), name='k(x)=cos(x)')]
graphs_visible = [go.Scatter(visible = True, x=x, y=f(x), name='f(x)=x<sup>2</sup>'),
go.Scatter(visible = True, x=x, y=x, name='g(x)=x'),
go.Scatter(visible = True, x=x, y=h(x), name='h(x)=sin(x)'),
go.Scatter(visible = True, x=x, y=k(x), name='k(x)=cos(x)')]
fig = go.Figure(data=graphs_invisible)
frames=[]
for i in range(len(graphs_visible)+1):
frames.append(go.Frame(name=str(i),
data=graphs_visible[:i]+graphs_invisible[i:]))
steps = []
for i in range(len(graphs_visible)+1):
step = dict(
label = str(i),
method = "animate",
args = [[str(i)]]
)
steps.append(step)
sliders = [dict(
currentvalue = {"prefix": "Графиков отображается: ", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
steps = steps,
)]
fig.update_layout(title="Выводим графики по очереди",
xaxis_title="Ось X",
yaxis_title="Ось Y",
updatemenus=[dict(direction="left",
pad = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="▶", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
fig.layout.sliders = sliders
fig.frames = frames
fig.show()
Для полноты картины рассмотрим несколько других способов визуализации данных, кроме линейных графиков. Начнём с круговых диаграмм
Для нашего эксперимента "подбросим" 100 раз пару игральных кубиков (костей) и запишем суммы выпавших очков.
dices = pd.DataFrame(np.random.randint(low=1, high=7, size=(100, 2)), columns=('Кость 1', 'Кость 2'))
dices['Сумма'] = dices['Кость 1'] + dices['Кость 2']
# Первые 5 бросков игральных костей
display(dices.head())
sum_counts = dices['Сумма'].value_counts().sort_index()
# количество выпавших сумм
display(sum_counts)
Для того чтобы создать круговую диаграмму используем go.Pie, который добавляем так же, как мы добавляли график на созданную фигуру.
Используем 2 основных атрибута:
values - размер сектора диаграммы, в нашем случае прямо пропорционален количеству той или иной суммы
labels - подпись сектора, в нашем случае значение суммы. Если не передать подпись, то в качестве подписи будет взят индекс значения из списка values
fig = go.Figure()
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index))
fig.show()
Сразу бросается в глаза то, что хотя мы передали массив, упорядоченный по индексам, но при построении он был пересортирован по значениям.
Это легко исправить с помощью аргумента
sort = False
fig = go.Figure()
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, sort = False))
fig.show()
Так же при желании мы можем "выдвинуть" один или несколько секторов.
Для этого используем аргумент pull, который принимаем список чисел. Каждое число - доля, на которую надо выдвинуть сектор из круга:
0 - не выдвигать
1 - 100% радиуса круга
Мы создадим список из нулей, такой же длинны, что массив значений. А потом один элемент увеличим до 0.2.
Обратите внимание, мы не используем метод idxmax Pandas, т.к. наш массив имеет индексы, соответствующие суммам. А определение какой сектор выдвигать на диаграмме происходит по индексу списка, к которому наш массив приводится.
fig = go.Figure()
pull = [0]*len(sum_counts)
pull[sum_counts.tolist().index(sum_counts.max())] = 0.2
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull))
fig.show()
Если вам не нравятся классические круговые диаграммы "пирожки", то легко превратить их в "пончики", вырезав сердцевину. Для этого используем аргумент hole, в который передаём число (долю радиуса, которую надо удалить):
0 - не вырезать ничего
1 - 100% вырезать, ничего не оставить
Таким образом, значение 0.9 превратит круговую диаграмму в кольцевую.
fig = go.Figure()
pull = [0]*len(sum_counts)
pull[sum_counts.tolist().index(sum_counts.max())] = 0.2
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9))
fig.show()
Кстати, образовавшаяся "дырка от бублика" - идеальное место для подписи, которую можно сделать с помощью атрибута annotations слоя.
Не забываем, что аннотаций может быть много, поэтому annotations принимаем список словарей.
Текст аннотации поддерживает HTML разметку (чем мы воспользуемся, задав абсурдно длинный текст, не помещающийся в 1 строку)
fig = go.Figure()
pull = [0]*len(sum_counts)
pull[sum_counts.tolist().index(sum_counts.max())] = 0.2
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9))
fig.update_layout(
annotations=[dict(text='Суммы очков<br>при броске<br>2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)])
fig.show()
Естественно обычный способы оформления визуализаций, показанные для графиков, тут тоже работают:
title
title_x
margin
legend_orientation
fig = go.Figure()
pull = [0]*len(sum_counts)
pull[sum_counts.tolist().index(sum_counts.max())] = 0.2
fig.add_trace(go.Pie(values=sum_counts, labels=sum_counts.index, pull=pull, hole=0.9))
fig.update_layout(
title="Пример кольцевой/круговой диаграммы",
title_x = 0.5,
margin=dict(l=0, r=0, t=30, b=0),
legend_orientation="h",
annotations=[dict(text='Суммы очков<br>при броске<br>2 игральных костей', x=0.5, y=0.5, font_size=20, showarrow=False)])
fig.show()
Что, если вы хотим детализовать картинку?
Нам на помощь приходит диаграмма "солнечные лучи" - иерархическая диаграмма на основе круговой. По сути это набор кольцевых диаграмм, нанизанных друг на друга, причём сегменты следующего уровня находятся в пределах границ сегментов своего "родителя" на предыдущем.
Например, получить 8 очков с помощью 2 игральных костей можно несколькими способами:
2 + 6
3 + 5
4 + 4
Для построения диаграммы нам потребуется go.Sunburst и 4 основных аргумента:
values - значения, задающие долю от круга на диаграмме
branchvalues="total" - такое значение указывает, что значения родительского элемента являются суммой значений потомков. Это необходимо для того, чтобы составить полный круг на каждом уровне.
labels - список подписей, которые отображаются на диаграмме
parents - список подписей родителей, для построения иерархии. Для элементов 0 уровня (без родителей) родителем указывается пустая строка.
Для начала обойдёмся 2 уровнями (все события и суммы)
# 1-й уровень, центр диаграммы
labels = ["Всего событий: " + str(sum(sum_counts))]
parents = [""]
values = [sum(sum_counts)]
# 2-й уровень, "лепестки" диаграммы
second_level_dict = {x:'Событий: ' + str(sum_counts[x]) + '<br>Σ = ' + str(x) for x in sum_counts.index}
labels += map(lambda x: second_level_dict[x], sum_counts.index)
parents += [labels[0]]*len(sum_counts)
values += sum_counts.tolist()
fig = go.Figure(go.Sunburst(
labels = labels,
parents = parents,
values=values,
branchvalues="total"
))
#fig.update_layout(margin = dict(t=0, l=0, r=0, b=0))
fig.show()
А теперь добавим группировку по парам исходов игральных костей и вычисление для таких пар "родителей".
Конечно, если кости идентичны, то 6+2 и 2+6 - это идентичные исходы, как и пара 3+5 и 5+3, но в рамках следующего примера мы будем считать их разными, просто чтобы не добавлять лишнего кода.
Так же уменьшим отступы, т.к. подписи получаются уж очень мелкими.
# 1-й уровень, центр диаграммы
labels = ["Всего событий: " + str(sum(sum_counts))]
parents = [""]
values = [sum(sum_counts)]
# 2-й уровень, "промежуточный"
second_level_dict = {x:'Событий: ' + str(sum_counts[x]) + '<br>Σ = ' + str(x) for x in sum_counts.index}
labels += map(lambda x: second_level_dict[x], sum_counts.index)
parents += [labels[0]]*len(sum_counts)
values += sum_counts.tolist()
# Готовим DataFrame для 3 уровня (группируем )
third_level = dices.groupby(['Кость 1', 'Кость 2']).count().reset_index()
third_level.rename(columns={'Сумма':'Value'}, inplace=True)
third_level['Сумма'] = third_level['Кость 1'] + third_level['Кость 2']
third_level['Label'] = third_level['Кость 1'].map(str) + ' + ' + third_level['Кость 2'].map(str)
third_level['Parent'] = third_level['Сумма'].map(lambda x: second_level_dict[x])
# 3-й уровень, "лепестки" диаграммы
values += third_level['Value'].tolist()
parents += third_level['Parent'].tolist()
labels += third_level['Label'].tolist()
fig = go.Figure(go.Sunburst(
labels = labels,
parents = parents,
values=values,
branchvalues="total"
))
fig.update_layout(margin = dict(t=0, l=0, r=0, b=0))
fig.show()
Естественно не круговыми диаграммами едиными, иногда нужны и обычные столбчатые.
Простейшая гистограмма строится с помощью go.Histogram. В качестве единственного аргумента в x передаём список значений, которые участвуют в выборке (Plotly самостоятельно сгруппирует их в столбцы и вычислит высоту), в нашем случае это колонка с суммами.
fig = go.Figure(data=[go.Histogram(x=dices['Сумма'])])
fig.show()
Если по какой-то причине нужно построить не вертикальную, а горизонтальную гистограмму, то меняем x на y:
fig = go.Figure(data=[go.Histogram(y=dices['Сумма'])])
fig.show()
А что, если у нас 2 или 3 набора данных и мы хотим их сравнить? Сгенерируем ещё 1100 бросков пар кубиков и просто добавим на фигуру 2 гистограммы:
dices2 = pd.DataFrame(np.random.randint(low=1, high=7, size=(100, 2)), columns=('Кость 1', 'Кость 2'))
dices2['Сумма'] = dices2['Кость 1'] + dices2['Кость 2']
dices3 = pd.DataFrame(np.random.randint(low=1, high=7, size=(1000, 2)), columns=('Кость 1', 'Кость 2'))
dices3['Сумма'] = dices3['Кость 1'] + dices3['Кость 2']
fig = go.Figure()
fig.add_trace(go.Histogram(x=dices['Сумма']))
fig.add_trace(go.Histogram(x=dices2['Сумма']))
fig.add_trace(go.Histogram(x=dices3['Сумма']))
fig.show()
Все 3 выборки подчиняются одному и тому же распределению, и очевидно, но количество событий сильно отличается, поэтому на нашей гистограмме некоторые столбцы сильно больше других.
Картинку надо "нормализовать". Для этого служит аргумент histnorm.
fig = go.Figure()
fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density'))
fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density'))
fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density'))
fig.show()
Как и предыдущие виды визуализаций, гистограммы могут иметь оформление:
подпись графика, подписи осей
ориентация и положение легенды.
отступы
fig = go.Figure()
fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', name='100 бросков v.1'))
fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', name='100 бросков v.2'))
fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', name='1000 бросков'))
fig.update_layout(
title="Пример гистограммы на основе бросков пары игральных костей",
title_x = 0.5,
xaxis_title="сумма очков",
yaxis_title="Плотность вероятности",
legend=dict(x=.5, xanchor="center", orientation="h"),
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Другой интересны режим оформления - barmode='overlay' - он позволяет рисовать столбцы гистограммы одни поверх других.
Имеет смысл использовать его одновременно с аргументом opacity самих гистограмм - он задаёт прозрачность гистограммы (от 0 до 100%).
Однако, большое количество гистограмм в таком случае тяжело визуально интерпретировать, поэтому мы скроем одну.
fig = go.Figure()
fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', opacity=0.75, name='100 бросков v.1'))
fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', opacity=0.75, name='100 бросков v.2'))
#fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', opacity=0.75, name='1000 бросков'))
fig.update_layout(
title="Пример гистограммы на основе бросков пары игральных костей",
title_x = 0.5,
xaxis_title="сумма очков",
yaxis_title="Плотность вероятности",
legend=dict(x=.5, xanchor="center", orientation="h"),
barmode='overlay',
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Если мы говорим о вероятности, то имеет так же смысл построить и накопительную гистограмму. Например, вероятности выпадения не менее чем X очков на сумме из 2 игральных костей.
Для этого используется аргумент гистограммы cumulative_enabled=True
fig = go.Figure()
fig.add_trace(go.Histogram(x=dices['Сумма'], histnorm='probability density', cumulative_enabled=True, name='100 бросков v.1'))
fig.add_trace(go.Histogram(x=dices2['Сумма'], histnorm='probability density', cumulative_enabled=True, name='100 бросков v.2'))
fig.add_trace(go.Histogram(x=dices3['Сумма'], histnorm='probability density', cumulative_enabled=True, name='1000 бросков'))
fig.update_layout(
title="Пример накопительной гистограммы на основе бросков пары игральных костей",
title_x = 0.5,
xaxis_title="сумма очков",
yaxis_title="Вероятность",
legend=dict(x=.5, xanchor="center", orientation="h"),
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Так же весьма полезно то, что на одной фигуре можно совмещать график, построенный по точкам (go.Scatter) и гистограмму (go.Histogram).
Для демонстрации такого применения, давайте сгенерируем 1000 событий из другого распределения - нормального. Для него легко построить теоретическую кривую. Мы возьмём для этого готовые функции из модуля scipy:
scipy.stats.norm.rvs - для генерации событий
scipy.stats.norm.pdf - для получения теоретический функции распределения
В качестве начального и конечного значений аргумента (x) возьмём границы интервала в 3σ
from scipy.stats import norm
r = norm.rvs(size=1000)
x_norm = np.linspace(norm.ppf(0.01), norm.ppf(0.99), 100)
fig = go.Figure()
fig.add_trace(go.Histogram(x=r, histnorm='probability density', name='"Экспериментальные" данные'))
fig.add_trace(go.Scatter(x=x_norm, y=norm.pdf(x_norm), name='Теоретическая форма нормального распределения'))
fig.update_layout(
title="Пример гистограммы на основе нормального распределения",
title_x = 0.5,
legend=dict(x=.5, xanchor="center", orientation="h"),
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Этот пример так же демонстрирует как происходит объединение в столбцы, если величина не дискретная.
В данном случае каждый столбец тем выше, чем больше значений попало в интервал, соответствующий ширине этого столбца.
В свою очередь это означает, что при необходимости мы можем регулировать количество столбцов и их ширину (это 2 взаимосвязанных параметра).
Вариант 1 - задав ширину столбца - xbins={"size":0.1}
Вариант 2 - задав количество столбцов - nbinsx=200
fig = go.Figure()
fig.add_trace(go.Histogram(nbinsx=200,
x=r, histnorm='probability density', name='"Экспериментальные" данные'))
fig.add_trace(go.Scatter(x=x_norm, y=norm.pdf(x_norm), name='Теоретическая форма нормального распределения'))
fig.update_layout(
title="Пример гистограммы на основе нормального распределения",
title_x = 0.5,
legend=dict(x=.5, xanchor="center", orientation="h"),
margin=dict(l=0, r=0, t=30, b=0))
fig.show()
Столбчатые диаграммы можно сформировать и своими силами, если сгруппировать данные и вычислить высоты столбцов.
Далее, используя класс go.Bar передаём названия столбцов и их величины в 2 аргумента:
x - подписи
y - величины
d_grouped = dices.groupby(['Сумма']).count()
labels = d_grouped.index
values = d_grouped['Кость 1'].values
fig = go.Figure(data=[go.Bar(x = labels, y = values)])
fig.show()
Важно!
Как и круговая диаграмма, такая столбчатая в отличие от ранее изученных гистограмм не построит столбец для того, чего нет!
Например, если мы сделаем только 10 бросков по 2 кости, то среди них не может выпасть всех возможных случаев. А значит, они не отобразятся на диаграмме:
BAD_d_grouped = dices.head(10).groupby(['Сумма']).count()
labels = BAD_d_grouped.index
values = BAD_d_grouped['Кость 1'].values
fig = go.Figure(data=[go.Bar(x = labels, y = values)])
fig.show()
При необходимости выведения ВСЕХ, даже нулевых столбцов, их следует сформировать самостоятельно.
Создадим парную гистограмму для 2 наборов по 100 бросков, в оба набора добавив на всякий случай колонки с нулями, если их нет.
В зависимости от генерации начальных данных в каких-то местах должна быть только 1 колонка, либо не будет колонок вообще.
А что, если требуется более сложный и информативный инструмент? Примером может служить диаграмма размаха или "ящик с усами" (https://habr.com/ru/post/267123/)
Для примера создадим набор 100 событий с бросками набора других игральных костей. На этот раз 3 4-гранных кости (3d4). Это могло бы быть сравнением 2 игровых мечей с уроном 2d6 и 3d4, однако, любому очевидно, что второй эффективнее (разброс 2-12 против разброса 3-12). Вся ли это информация, которую можно "вытащить" из этих данных?
Конечно нет, ведь у них будут отличаться и меры центральной тенденции (медианы или средние).
Для построения ящиков с усами мы используем класс go.Box. Данные (весь массив "сумм") передаём в единственный аргумент - y.
dices4 = pd.DataFrame(np.random.randint(low=1, high=5, size=(100, 3)), columns=('Кость 1', 'Кость 2', 'Кость 4'))
dices4['Сумма'] = dices4['Кость 1'] + dices4['Кость 2'] + dices4['Кость 4']
fig = go.Figure()
fig.add_trace(go.Box(y=dices['Сумма']))
fig.add_trace(go.Box(y=dices4['Сумма']))
fig.show()
Не совсем понятно кто есть кто.
Примечание. Т.к. мы используем random, то в вашем случае результат может получиться не такой, как у меня при тестовой генерации, однако забавно, что с первой же попытки во время подготовки этого материала я получил вот такую картинку:

Тут ясно, что "усы" левого ящика имеют размах 2-12, значит, это и есть 2d6. Но занятно, что хотя нижняя граница прямого "усы" выше левого, но и верхняя ниже! Это объясняется тем, что 100 событий не так уж и много, а выбросить сразу 3 четвёрки довольно сложно. И медианы у них на одном уровне. Выходит, наше первоначальное предположение о большей эффективности оружия с уроном 3d4 можно считать справедливым лишь по уровню 25% квартиля - он явно выше на правой картинке. Т.е. "ящик с усами" всё же дал нам довольно много легко считываемой и не совсем очевидной первоначально информации.
Однако, как и для других фигур, тут можно задать подписи.
fig = go.Figure()
fig.add_trace(go.Box(y=dices['Сумма'], name='2d6'))
fig.add_trace(go.Box(y=dices4['Сумма'], name='3d4'))
fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей",
xaxis_title="Вид испытаний",
yaxis_title="Cумма очков")
fig.show()
Иногда вертикальные ящики не очень наглядны (либо сложно прочитать подписи снизу), тогда их можно положить "на бок" так же, как мы делали с обычными столбчатыми диаграммами:
fig = go.Figure()
fig.add_trace(go.Box(x=dices['Сумма'], name='2d6'))
fig.add_trace(go.Box(x=dices4['Сумма'], name='3d4'))
fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей",
yaxis_title="Вид испытаний",
xaxis_title="Cумма очков")
fig.show()
Иногда полезно для каждого ящика с усами так же отобразить облако точек, формирующий распределение. Это легко сделать с помощью аргумента boxpoints='all'
fig = go.Figure()
fig.add_trace(go.Box(x=dices['Сумма'], name='2d6', boxpoints='all'))
fig.add_trace(go.Box(x=dices4['Сумма'], name='3d4', boxpoints='all'))
fig.update_layout(title="Сравнение испытаний по 100 бросков игральных костей",
yaxis_title="Вид испытаний",
xaxis_title="Cумма очков")
fig.show()
Plotly поддерживает великое множество разных видов визуализаций, охватить все из которых в одном обзоре довольно трудно (и бессмысленно, т.к. общие принципы будут схожи с ранее показанными)
Полезно будет в завершении лишь показать один из наиболее красивых на мой взгляд "графиков" - Scattermapbox - геокарты.
Для этого возьмём CSV с 1117 населёнными пунктами РФ и их координатами (файл создан на основе https://github.com/hflabs/city/blob/master/city.csv) - 'https://raw.githubusercontent.com/hflabs/city/master/city.csv.
Воспользуемся классом go.Scattermapbox и 2 атрибутами:
lat (широта)
lon (долгота)
Так же нам понадобится подключить OSM карту, т.к. Scattermapbox может работать с разными видами карт:
fig.update_layout(mapbox_style="open-street-map")
cities = pd.read_csv('https://raw.githubusercontent.com/hflabs/city/master/city.csv')
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon']))
fig.update_layout(mapbox_style="open-street-map")
fig.show()
Как-то криво, правда? Давайте сдвинем центр карты так, чтобы он пришёлся на столицу нашей родины (вернее столицу родины автора этих строк, т.к. у читателя родина может быть иной).
Для этого нам понадобится объект go.layout.mapbox.Center или обычный словарь с 2 аргументами:
lat
lon
Этот объект/словарь мы передаём в качестве значения аргумента center словаря внутрь mapbox:
fig.update_layout(
mapbox=dict(
center=dict(
lat=...,
lon=...
)
)
)
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon']))
capital = cities[cities['region']=='Москва']
map_center = go.layout.mapbox.Center(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0])
# Аналог с помощью словаря
#map_center = dict(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0])
fig.update_layout(mapbox_style="open-street-map", mapbox=dict(center=map_center))
fig.show()
Неплохо, но масштаб мелковат (по сути сейчас отображается карта мира на которой 1/6 часть суши занимает далеко не всё полезное место).
Без ущерба для полезной информации можно слегка приблизить картинку.
Для этого используем аргумент zoom=2
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon']))
capital = cities[cities['region']=='Москва']
map_center = go.layout.mapbox.Center(lat=capital['geo_lat'].values[0], lon=capital['geo_lon'].values[0])
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Увы, на карту попало слишком много Европы без данных и слишком мало отечественного дальнего востока, так что в данном случае центрироваться возможно стоит по геометрическому центру страны (вычислим его весьма "приблизительно").
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Давайте добавим подписи городов. Для этого используем аргумент text.
Следует заметить, что для нескольких населённых пунктов (города федерального значения) почему-то не заполнено поле city, поэтому для них мы его вручную заполним из address. Не очень красиво, но главное, что не пустота.
cities.loc[cities['city'].isna(), 'city'] = cities.loc[cities['city'].isna(), 'address']
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'], lon=cities['geo_lon'], text=cities['city']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Вспомним, как мы увеличивали плотность информации для обычных графиков. Тут так же можно задать размер маркера, например, от населения.
Следует учесть 2 момента:
Данные замусорены. Население некоторых городов имеет вид 96[3]. Поэтому колонка с население не численная и нам нужна функция, которая этот мусор обнулит, либо приведёт к какому-то читаемому виду.
Размер маркера задаётся в пикселях. И 15 миллионов пикселей - слишком большой диаметр. Потому разумно придумать формулу, например, логарифм.
def to_int_size(value):
try:
return np.log10(int(value))
except:
return np.log10(int(value.split('[')[0]))
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(size=cities['population'].map(to_int_size))))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Добавим цветовую кодировку. Для этого используем данные о годе основания. Т.к. не для всех городов он точно известен (для некоторых указан век, для некоторых римскими, а не арабскими цифрами), то мы так же вынуждены будем написать свою функцию для обработки годов, но для простоты все проблемные случаи мы будем возвращать None и потом просто удалим все такие города.
Если возвращать, например, np.NaN, то при построении тепловой карты эти значения будут считаться эквивалентными 0 и мы будем считать такие населённые пункты одними из самых старых в стране)
def to_int_year(value):
try:
return int(value)
except:
return None
cities['foundation_year'] = cities['foundation_year'].map(to_int_year)
cities = cities[['region', 'city', 'geo_lat', 'geo_lon', 'foundation_year', 'population']].dropna()
fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'],
size=cities['population'].map(to_int_size))))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
А что если мы хотим нанести линию? Без проблем!
Возьмём и добавим новый график на имеющуюся картинку, который будет содержать только 2 точки: Москву и Санкт-Петербург.
Нам понадобится новый атрибут mode = "lines" (у него доступны и другие значения, например "markers+lines"), но мы уже вывели метку города, так что не хотим её дублировать.
hoverinfo='skip'fig = go.Figure(go.Scattermapbox(lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'].map(to_int_year),
size=cities['population'].map(to_int_size))))
fig.add_trace(go.Scattermapbox(mode = "lines",
hoverinfo='skip',
lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'],
lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Ох, кажется полоска тепловой карты наложилась на легенду. Более того, теперь легенда выводится раздельная для точек-городов и линии между Москвой и Санкт-Петербургом.
Переключим легенду в горизонтальный режим legend_orientation="h" (в настройках слоя)
"сгруппируем" легенды вместе. Для этого у каждого графика группы добавим аргумент legendgroup="group" (можно использовать любые строки, лишь бы они были одинаковые у членов одной группы).
fig = go.Figure(go.Scattermapbox(legendgroup="group",
lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'].map(to_int_year),
size=cities['population'].map(to_int_size))))
fig.add_trace(go.Scattermapbox(legendgroup="group",
mode = "lines",
hoverinfo='skip',
lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'],
lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(legend_orientation="h",
mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Отлично, теперь они включаются и выключаются вместе. Давайте уберём из легенды "лишний" элемент (линию городов) showlegend=False
А так же подпишем легенду для городов.
fig = go.Figure(go.Scattermapbox(legendgroup="group",
name='Города России',
lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'].map(to_int_year),
size=cities['population'].map(to_int_size))))
fig.add_trace(go.Scattermapbox(legendgroup="group",
showlegend=False,
mode = "lines",
hoverinfo='skip',
lat=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lat'],
lon=cities[cities['region'].isin(['Санкт-Петербург', 'Москва'])]['geo_lon']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(legend_orientation="h",
mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Давайте добавим чуть более осмысленные линии на карту. Для этого воспользуемся маршрутом поезда №002М "Россия" Москва-Владивосток - https://pass.rzd.ru/timetable/public/ru?STRUCTURE_ID=735&layer_id=5370&refererLayerId=5354&train_num=002MJ&numtrain=002%D0%9C&src_code=2000000&departureDate=01.06.2014&firstStation=%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0%20%D0%AF%D1%80&lastStation=%D0%92%D0%BB%D0%B0%D0%B4%D0%B8%D0%B2%D0%BE%D1%81%D1%82
Я заранее подготовил отдельный файл с городами, на маршруте, разбитом по дням. Это примерная разбивка, т.к. расписание меняется, так что не используйте мою таблицу для оценка когда вы приедете к любимой тёще в гости. Некоторые станции поезда не имеют аналога в нашей оригинальной таблице городов, поэтому они пропущена. Некоторые города указаны 2 раза, т.к. они являются конечной точкой одного дневного перегона и начальной другого дневного перегона.
Наш маршрут будет соединять города, а не вокзалы, так же он не будет совпадать с реальной железной дорогой. Это просто визуализация маршрута, а не инструмент навигации!
train_russia = pd.read_csv('https://gist.githubusercontent.com/lexnekr/2da07b5fc12b5be24068e4d68ed47ca5/raw/d6256765a3223282fbfec7e0b040cbfb21593fff/train_russia.scv')
fig = go.Figure(go.Scattermapbox(legendgroup="group",
name='Города России',
lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'].map(to_int_year),
size=cities['population'].map(to_int_size))))
for df_for_today in train_russia.groupby(['day number']):
fig.add_trace(go.Scattermapbox(name='День {}'.format(df_for_today[0]),
mode = "lines",
hoverinfo='skip',
lat=df_for_today[1]['geo_lat'],
lon=df_for_today[1]['geo_lon']))
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(title='По России на поезде',
legend_orientation="h",
mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2))
fig.show()
Если мы хотим анимировать процесс появления маршрута по дням, то нам придётся использовать тот же приём, что и ранее с появлением нескольких графиков - заранее вывести все графики или их заглушки невидимыми, а потом на каждом фрейме и шаге слайдера делать их видимыми.
data = [go.Scattermapbox(legendgroup="group",
name='Города России',
lat=cities['geo_lat'],
lon=cities['geo_lon'],
text=cities['city'],
marker=dict(colorbar=dict(title="Год основания"),
color=cities['foundation_year'].map(to_int_year),
size=cities['population'].map(to_int_size)))]
for df_for_today in train_russia.groupby(['day number']):
data.append(go.Scattermapbox(visible=False,
name='День {}'.format(df_for_today[0]),
mode = "lines",
hoverinfo='skip',
lat=df_for_today[1]['geo_lat'],
lon=df_for_today[1]['geo_lon']))
fig = go.Figure(data)
frames=[]
for i in range(len(data)+1):
temp_frame = go.Frame(name=str(i), data=data)
for j in range(1, i):
temp_frame['data'][j]['visible']=True
frames.append(temp_frame)
steps = []
for i in range(len(data)):
step = dict(
label = str(i),
method = "animate",
args = [[str(i+1)]]
)
steps.append(step)
sliders = [dict(
currentvalue = {"prefix": "День №", "font": {"size": 20}},
len = 0.9,
x = 0.1,
pad = {"b": 10, "t": 50},
steps = steps,
)]
map_center = go.layout.mapbox.Center(lat=(cities['geo_lat'].max()+cities['geo_lat'].min())/2,
lon=(cities['geo_lon'].max()+cities['geo_lon'].min())/2)
fig.update_layout(title='По России на поезде',
legend_orientation="h",
mapbox_style="open-street-map",
mapbox=dict(center=map_center, zoom=2),
updatemenus=[dict(direction="left",
pad = {"r": 10, "t": 80},
x = 0.1,
xanchor = "right",
y = 0,
yanchor = "top",
showactive=False,
type="buttons",
buttons=[dict(label="▶", method="animate", args=[None, {"fromcurrent": True}]),
dict(label="❚❚", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])])],
)
fig.layout.sliders = sliders
fig.frames = frames
fig.show()
Безусловно мы разобрали далеко не все виды графиков Plotly. Однако, данного базового набора примеров должно быть достаточно чтобы понять принцип по которому все они работают.
С примерами других визуализаций можно ознакомиться тут - https://plotly.com/python/ (обратите внимание, что для каждой категории приведены далеко не все примеры, больше примеров всегда доступно по ссылке "More ..."